package com.magenta.maxoptra.gis.distancematrix.builder;

import com.magenta.maxoptra.gis.GeoProviderSettings;
import com.magenta.maxoptra.gis.commons.Pair;
import com.magenta.maxoptra.gis.distancematrix.DistanceMatrixCoordinate;
import com.magenta.maxoptra.gis.distancematrix.RoutesIterator;
import com.magenta.maxoptra.gis.distancematrix.StateAwareRouteIterator;
import com.magenta.maxoptra.gis.util.Apis;
import org.jetbrains.annotations.Nullable;

import java.util.List;
import java.util.Map;

/**
 * Group with specific coordinates and settings of geo-provider
 *
 * @param <GID> type of group id
 * @author Sergey Grachev
 */
final class MatrixGroup<GID> {

    private final GID groupId;
    private final GeoProviderSettings primarySettings;
    private final long providerCid;
    private final int coordinatesLimitPerBatchUpdate;
    private final boolean allowBatchUpdates;

    private int requestsDelay;
    private long lastAccess = 0;
    private RoutesIterator routesIterator;
    private long expectedTimeOfProgressUpdate;
    private boolean withErrors;
    private long priority;
    private Pair<DistanceMatrixCoordinate, List<DistanceMatrixCoordinate>> awaitRoutes;
    private Pair<DistanceMatrixCoordinate, DistanceMatrixCoordinate> awaitRoute;

    private MatrixGroup(final GID groupId, final GeoProviderSettings primarySettings, final int requestsDelay,
                        final int coordinatesLimitPerBatchUpdate, final boolean allowBatchUpdates, final long priority) {
        this.groupId = groupId;
        this.primarySettings = primarySettings;
        this.requestsDelay = requestsDelay;
        this.coordinatesLimitPerBatchUpdate = coordinatesLimitPerBatchUpdate;
        this.allowBatchUpdates = allowBatchUpdates;
        this.priority = priority;
        this.providerCid = Apis.cid(primarySettings.getProviderName(), primarySettings.getVersionApi(), primarySettings.getCidDiscriminator());
        this.withErrors = false;
    }

    /**
     * @param groupId                        as is
     * @param settings                       as is
     * @param requestsDelay                  delay between routing requests
     * @param coordinatesLimitPerBatchUpdate maximum amount of coordinates in a request of batch matrix update to geo provider
     * @param allowBatchUpdates              allow or not batch updates using geo provider (if is supported)
     * @param priority                       priority of group, can be change during update process
     * @return new instance of group
     */
    public static <GID> MatrixGroup<GID> newInstance(final GID groupId, final GeoProviderSettings settings,
                                                     final int requestsDelay, final int coordinatesLimitPerBatchUpdate,
                                                     final boolean allowBatchUpdates, final long priority) {
        return new MatrixGroup<GID>(groupId, settings, requestsDelay, coordinatesLimitPerBatchUpdate, allowBatchUpdates,
                priority);
    }

    public GID getGroupId() {
        return groupId;
    }

    public GeoProviderSettings getGeoProviderSettings() {
        return primarySettings;
    }

    public long getProviderCid() {
        return providerCid;
    }

    public int getRequestsDelay() {
        return requestsDelay;
    }

    public void setRequestsDelay(final int requestsDelay) {
        this.requestsDelay = requestsDelay;
    }

    public void setRoutesIterator(final RoutesIterator routesIterator) {
        this.routesIterator = routesIterator;
    }

    public boolean isCoordinatesFetched() {
        return routesIterator != null;
    }

    public boolean hasFinishedUpdateForCoordinates() {
        return routesIterator != null && routesIterator.hasFinishedCoordinates();
    }

    @Nullable
    public Map<RoutesIterator.Type, List<DistanceMatrixCoordinate>> getFinishedUpdateFor() {
        return routesIterator != null ? routesIterator.getFinishedCoordinates() : null;
    }

    /**
     * Has next route
     *
     * @return true if next route is available
     */
    public boolean hasNextRoute() {
        return routesIterator.hasNext();
    }

    public int getCoordinatesLimitPerBatchUpdate() {
        return coordinatesLimitPerBatchUpdate;
    }

    /**
     * Calculate remaining delay between requests
     *
     * @return delay (milliseconds)
     */
    public long getRemainingRequestsDelay() {
        final long delta = System.currentTimeMillis() - lastAccess;
        return delta > requestsDelay ? 0L : requestsDelay - delta;
    }

    /**
     * Support for calculate the distances and times between a number of stops
     */
    public boolean isAllowBatchUpdates() {
        return allowBatchUpdates;
    }

    /**
     * Get next route
     */
    @Nullable
    public Pair<DistanceMatrixCoordinate, DistanceMatrixCoordinate> nextRoute() {
        lastAccess = System.currentTimeMillis();
        return routesIterator.nextRoute();
    }

    public Pair<DistanceMatrixCoordinate, List<DistanceMatrixCoordinate>> nextMatrix(final int limit) {
        lastAccess = System.currentTimeMillis();
        return routesIterator.nextRoutes(limit);
    }

    public void saveRoutesIteratorState() {
        if (routesIterator != null && routesIterator instanceof StateAwareRouteIterator) {
            ((StateAwareRouteIterator) routesIterator).saveState();
        }
    }

    public void restoreRoutesIteratorState() {
        if (routesIterator != null && routesIterator instanceof StateAwareRouteIterator) {
            ((StateAwareRouteIterator) routesIterator).restoreState();
        }
    }

    public long getTotalCoordinates() {
        return routesIterator != null ? routesIterator.size() : 0;
    }

    public long getCurrentCoordinateIndex() {
        return routesIterator != null ? routesIterator.index() : 0;
    }

    public boolean checkAndSetReadyForProgressUpdate(final long delayBeforeNextUpdate) {
        final long now = System.currentTimeMillis();
        if (expectedTimeOfProgressUpdate - now <= 0) {
            this.expectedTimeOfProgressUpdate = now + delayBeforeNextUpdate;
            return true;
        }
        return false;
    }

    public void markWithErrors() {
        withErrors = true;
    }

    public boolean isWithErrors() {
        return withErrors;
    }

    public long getPriority() {
        return priority;
    }

    public void setPriority(final long priority) {
        this.priority = priority;
    }

    public void awaitProviderAvailabilityRoutes(@Nullable final Pair<DistanceMatrixCoordinate, List<DistanceMatrixCoordinate>> routes) {
        this.awaitRoutes = routes;
    }

    public boolean isAwaitProviderAvailabilityRoutes() {
        return this.awaitRoutes != null;
    }

    @Nullable
    public Pair<DistanceMatrixCoordinate, List<DistanceMatrixCoordinate>> getAwaitRoutes() {
        return this.awaitRoutes;
    }

    public void awaitProviderAvailabilityRoute(@Nullable final Pair<DistanceMatrixCoordinate, DistanceMatrixCoordinate> route) {
        this.awaitRoute = route;
    }

    public boolean isAwaitProviderAvailabilityRoute() {
        return this.awaitRoute != null;
    }

    @Nullable
    public Pair<DistanceMatrixCoordinate, DistanceMatrixCoordinate> getAwaitRoute() {
        return this.awaitRoute;
    }

    @Override
    public String toString() {
        return "MatrixGroup{" +
                "groupId=" + groupId +
                ", settings=" + primarySettings +
                ", requestsDelay=" + requestsDelay +
                ", providerCid=" + providerCid +
                ", expectedTimeOfProgressUpdate=" + expectedTimeOfProgressUpdate +
                ", withErrors=" + withErrors +
                ", priority=" + priority +
                '}';
    }
}
